/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package br.ufrn.engcomp.mpc;

import br.ufrn.engcomp.gui.MainWindow;
import br.ufrn.engcomp.mpc.domain.*;
import br.ufrn.engcomp.mpc.domain.exceptions.PlantCommunicationException;
import br.ufrn.engcomp.observer.ConstrainedObserver;
import br.ufrn.engcomp.observer.LinearObserver;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
 *
 * @author Victor
 */
public class FThread {

    // GUI reference
    private MainWindow ui;
    // Time control
    private long time;
    private int tS;
    // Flag to determine whether the main loop is activated
    private boolean threadActivated;
    // Graphics
    private XYSeriesCollection datasetWrite, datasetRead;
    private JFreeChart chartWrite, chartRead;
    private XYSeries signalU;
    private XYSeries signalSetPoint, signalT1, signalT2;
    private XYSeries obsSignalT1, obsSignalT2; // OBSERVADOR
    private int numSamples;
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
    private static final int MAX_TESTE_RUIDO_TEMPO_SUBIDA = 1;
    private static final int MAX_TESTE_RUIDO_TEMPO_PICO = 5;
    private static final int MAX_TESTE_RUIDO_TEMPO_ACOMODACAO = 15;
    private double setPointAnterior;
    private double valorRespostaAnterior;
    private int sinalDerivadaAnterior;
    private boolean cruzouReferencia;
    private Double sobressinalMaximo;
    private Long tempoPico;
    private Long tempoAcomodacao;
    private int contTesteRuidoTempoSubida;
    private int contTesteRuidoTempoPico;
    private int contTesteRuidoTempoAcomodacao;
    private boolean tirouPS40;
    private boolean tirouPS60;
    private boolean tirouPS80;
    // Time control variables
    private Timer timer;
    private long inicioExecucao;
    private Map<TimeStamp, Long[]> timeStamps;
    // Observador de Allan
    private ConstrainedObserver constrObserver;
    //private LinearObserver constrObserver;

    private enum TimeStamp {

        LEITURA,
        CONTROLE,
        ESCRITA,
        LOOP
    }
    // Plant
    private Plant plant;
    private double levelT1, levelT2; // State variables: levels of tank 1 and tank 2
    private double y; // Output
    // Controller
    private PredictiveController javaPredictiveController;
    private PredictiveController matlabPredictiveController;
    private PredictiveController hybridPredictiveController;
    private PredictiveController pidFakePredictiveController;
    private PredictiveController currentPredictiveController;
    private double setPoint; // Required set-point
    private double step; // Last step value
    private double deltaU; // Control signal computed by predictive controller
    private double u; // Control signal sent to plant
    private double uMA; // Open loop control signal sent do plant
    // Disturb Observer
    private DisturbObserver disturbObserver;

    public FThread(MainWindow ui) {
        this.ui = ui;

        // OBSERVADOR
        u = 0.7;
        constrObserver = new ConstrainedObserver();
        //constrObserver = new LinearObserver(0.7624,0.2614);

//        plant = new CoupledTanksPlantWithConstraints();

        tS = 500; // Periodo de amostragem: 500 ms
//        time = 0;
        timeStamps = new EnumMap<TimeStamp, Long[]>(TimeStamp.class);

//        initJavaPredictiveController();
//        initMatlabPredictiveController();
//        initHybridPredictiveController();

//        // Utilizacao do controlador preditivo implementado em Java
//        currentPredictiveController = javaPredictiveController;

//        // NOVO
//        observer = new DisturbObserver(new CoupledTanksPlantWithDisturb());

        // Instancia os objetos referentes aos graficos
        initPlots();
    }

    private void initJavaPredictiveController() {
        double tempoInicio = System.currentTimeMillis();
        javaPredictiveController = new JavaSSPredictiveController(plant);
        double tempoFim = System.currentTimeMillis();
        System.out.println("Java levou " + ((tempoFim - tempoInicio) / 1000d) + " segundos para inicializar o ambiente.");
    }

    private void initMatlabPredictiveController() {
        double tempoInicio = System.currentTimeMillis();
        matlabPredictiveController = new MATLABSSPredictiveController(plant); // MATLAB controller version
        double tempoFim = System.currentTimeMillis();
        System.out.println("MATLAB levou " + ((tempoFim - tempoInicio) / 1000d) + " segundos para inicializar o ambiente.");
    }

    private void initHybridPredictiveController() {
        double tempoInicio = System.currentTimeMillis();
        hybridPredictiveController = new HybridSSPredictiveController(plant);
        double tempoFim = System.currentTimeMillis();
        System.out.println("Hybrid levou " + ((tempoFim - tempoInicio) / 1000d) + " segundos para inicializar o ambiente.");
    }

    private void initPIDFakePredictiveController() {
        double tempoInicio = System.currentTimeMillis();
        pidFakePredictiveController = new JavaFakePredictiveControllerPID(plant);
        double tempoFim = System.currentTimeMillis();
        System.out.println("PIFake levou " + ((tempoFim - tempoInicio) / 1000d) + " segundos para inicializar o ambiente.");
    }

    public JFreeChart getChartEscrita() {
        return chartWrite;
    }

    public JFreeChart getChartLeitura() {
        return chartRead;
    }

//    private boolean connect(String ip, int port) throws Exception {
//        try {
//            plant.connect(ip, port);
//            
//            return true;
//        } catch (Exception ex) {
//            throw ex;
//        }
//    }
    public void updateControllerParam() {
        updateControllerParam(ui.getHw(), ui.getHp(), ui.getHu(), ui.getQ(), ui.getR());
    }

    public void updateControllerParam(int hw, int hp, int hu, double[] q, double[] r) {
        if (currentPredictiveController != null) {
            currentPredictiveController.setParameters(hw, hp, hu, q, r);
        } else {
            uMA = ui.getEntrada();
        }
    }

    private void initPlots() {
        numSamples = 1000;

        signalU = new XYSeries("Entrada (u)");
        signalU.setMaximumItemCount(numSamples);

        datasetWrite = new XYSeriesCollection();
        datasetWrite.addSeries(signalU);

        chartWrite = ChartFactory.createXYLineChart(
                null, // chart title
                "Tempo (s)", // x axis label
                "Tensão (v)", // y axis label
                datasetWrite, // data
                PlotOrientation.VERTICAL,
                false, // include legend
                true, // tooltips
                false // urls
                );

        signalSetPoint = new XYSeries("Referência");
        signalT1 = new XYSeries("Tanque 1");
        signalT2 = new XYSeries("Tanque 2");

        // OBSERVADOR
        obsSignalT1 = new XYSeries("Tanque 1 (estimado)");
        obsSignalT2 = new XYSeries("Tanque 2 (estimado)");

        signalSetPoint.setMaximumItemCount(numSamples);
        signalT1.setMaximumItemCount(numSamples);
        signalT2.setMaximumItemCount(numSamples);

        // OBSERVADOR
        obsSignalT1.setMaximumItemCount(numSamples);
        obsSignalT2.setMaximumItemCount(numSamples);

        datasetRead = new XYSeriesCollection();
        datasetRead.addSeries(signalSetPoint);
        datasetRead.addSeries(signalT1);
        datasetRead.addSeries(signalT2);

        // OBSERVADOR
        datasetRead.addSeries(obsSignalT1);
        datasetRead.addSeries(obsSignalT2);


        chartRead = ChartFactory.createXYLineChart(
                null, // chart title
                "Tempo (s)", // x axis label
                "Altura (cm)", // y axis label
                datasetRead, // data
                PlotOrientation.VERTICAL,
                true, // include legend
                true, // tooltips
                false // urls
                );

        XYItemRenderer xy = chartRead.getXYPlot().getRenderer();
        xy.setSeriesPaint(0, java.awt.Color.YELLOW); // Cor do sinal de referência
        xy.setSeriesPaint(1, java.awt.Color.BLUE); // Cor do sinal do tanque 1 (superior)
        xy.setSeriesPaint(2, java.awt.Color.RED); // Cor do sinal do tanque 2 (inferior)

        // OBSERVADOR
        xy.setSeriesPaint(3, java.awt.Color.BLACK); // Cor do sinal do tanque 2 (inferior)
        xy.setSeriesPaint(4, java.awt.Color.PINK); // Cor do sinal do tanque 2 (inferior)
    }

    public void turnON(TipoPlanta tipoPlanta, boolean comRestricoes, TipoControlador tipoControlador, String ip, int port) throws PlantCommunicationException {
        synchronized (this) {
            clearTime();

            zerarEspecificacoesDesempenho();

            initPlant(tipoPlanta, comRestricoes);

            disturbObserver = new DisturbObserver(plant);
            lastDeltaXObserver = new double[]{0, 0};

            initController(tipoControlador);

            plant.connect(ip, port);

            // Reiniciar os graficos
            clearPlots();

            // Reiniciar as marcações de tempo
            clearTimeStamps();

            // Habilitar execucao da thread
            threadActivated = true;

            // Iniciar timer
            initTimer();

            // Atualizar os parametros do controlador
            updateControllerParam();
        }
    }

    public void turnOFF() {
        synchronized (this) {
            threadActivated = false;
            try {
                // Enviar valor de tensao ZERO
                plant.shutdown();
                setPoint = 0;
                deltaU = 0;
                u = 0;
                timer.cancel();
            } catch (PlantCommunicationException ex) {
                Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    private void initPlant(TipoPlanta tipoPlanta, boolean comRestricoes) {
        switch (tipoPlanta) {
            case Modelo:
                if (comRestricoes) {
                    plant = new CoupledTanksPlantModelWithConstraints();
                } else {
                    plant = new CoupledTanksPlantModel();
                }
                break;
            case Simulador:
                if (comRestricoes) {
                    plant = new CoupledTanksPlantWithConstraints();
                } else {
                    plant = new CoupledTanksPlant();
                }
                break;
            case PlantaReal:
                if (comRestricoes) {
                    plant = new CoupledTanksPlantWithConstraints();
                } else {
                    plant = new CoupledTanksPlant();
                }
                break;
            default:
                throw new RuntimeException("Tipo de planta inválida.");
        }

        deltaUBarra = plant.getInputOP()[0];
        deltaXBarra = new double[]{plant.getStateOP()[0], plant.getStateOP()[1]};
        deltaYBarra = deltaXBarra[1];
    }

    private void initController(TipoControlador tipoControlador) {
        switch (tipoControlador) {
            case Java:
                initJavaPredictiveController();
                currentPredictiveController = javaPredictiveController;
                break;
            case Matlab:
                initMatlabPredictiveController();
                currentPredictiveController = matlabPredictiveController;
                break;
            case Hybrid:
                initHybridPredictiveController();
                currentPredictiveController = hybridPredictiveController;
                break;
            case PID:
                initPIDFakePredictiveController();
                currentPredictiveController = pidFakePredictiveController;
                break;
            case MalhaAberta:
                currentPredictiveController = null;
                break;
            default:
                throw new RuntimeException("Tipo de controlador inválido.");
        }

        deltaU = 0;
        u = 0;
    }

    // Salvar screenshot dos graficos em JPEG
    public void takeScreenshot() {
        if (currentPredictiveController != null) {
            try {
                Object[] parameters = currentPredictiveController.getParameters();

                // Salva as imagens no mesmo diretorio do projeto
                String id = SDF.format(new Date(inicioExecucao));
                String sp = String.valueOf(setPoint);
                String hw = String.valueOf((Integer) parameters[0]);
                String hp = String.valueOf((Integer) parameters[1]);
                String hu = String.valueOf((Integer) parameters[2]);
                String q = String.valueOf(((double[]) parameters[3])[0]);
                String r = String.valueOf(((double[]) parameters[4])[0]);
                String timeSimulation = String.valueOf(time / 1000d);

                String parametros = "SP=" + sp + "; Hp=" + hp + "; Hu=" + hu + "; Hw=" + hw + "; Q=" + q + "; R=" + r + "; Tempo=" + timeSimulation;
                String especificoes = "tp=" + (tempoPico != null ? String.format("%.2f", tempoPico / 1000d) + "s" : "-")
                        + "; Mp=" + (sobressinalMaximo != null ? String.format("%.2f", sobressinalMaximo) + "%" : "-")
                        + "; tss=" + (tempoAcomodacao != null ? String.format("%.2f", tempoAcomodacao / 1000d) + "s" : "-")
                        + "; ess=" + (tempoAcomodacao != null ? String.format("%.2f", arredondar(100 - (y / this.setPoint) * 100, 2)) + "%" : "-");
                String sinais = "U=" + String.format("%.2f", u) + "V; T1=" + String.format("%.2f", levelT1) + "cm; T2=" + String.format("%.2f", levelT2) + "cm";

                ChartUtilities.saveChartAsJPEG(new java.io.File("./ps/" + id + " " + plant.getCodeName() + " [" + especificoes + "] - [" + parametros + "] - [" + sinais + "] Controle.jpg"), 1.0f, this.chartWrite, 640, 480);
                ChartUtilities.saveChartAsJPEG(new java.io.File("./ps/" + id + " " + plant.getCodeName() + " [" + especificoes + "] - [" + parametros + "] - [" + sinais + "] Saída.jpg"), 1.0f, this.chartRead, 640, 480);
            } catch (IOException ex) {
                Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    private void clearTime() {
        time = 0;
        inicioExecucao = System.currentTimeMillis();
    }

    private void clearPlots() {
        signalSetPoint.clear();
        signalT1.clear();
        signalT2.clear();
        signalU.clear();

        // OBSERVADOR
        obsSignalT1.clear();
        obsSignalT2.clear();
    }

    private void clearTimeStamps() {
        timeStamps.put(TimeStamp.LEITURA, new Long[]{0L, 0L});
        timeStamps.put(TimeStamp.CONTROLE, new Long[]{0L, 0L});
        timeStamps.put(TimeStamp.ESCRITA, new Long[]{0L, 0L});
        timeStamps.put(TimeStamp.LOOP, new Long[]{0L, 0L});
    }

    // Timer
    public final void initTimer() {
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                fRun();
            }
        };

        timer = new Timer();

        timer.schedule(task, 0, tS);
    }

    // Trava de seguranca
    private void limitarNivelT1() {
        // Trava superior
        if (levelT1 >= Constants.NIVEL_MAX && u > 0.0) {
            System.out.println("Trava superior em ação");
            u = 0.0;
        }

        // Trava inferior
        if (levelT1 < Constants.NIVEL_MIN && u < 0.0) {
            System.out.println("Trava inferior em ação");
            u = 0.0;
        }
        
    }

    private void saturarSinal() {
        if (u > Constants.TENSAO_SATURACAO) {
            u = Constants.TENSAO_SATURACAO;
        } else if (u < -Constants.TENSAO_SATURACAO) {
            u = -Constants.TENSAO_SATURACAO;
        }
    }
//    double[] lastDeltaXModel = new double[]{ 0, 0 };
//    double[] lastDeltaYModel;
    double[] lastDeltaXObserver;
    double obsLevelT1, obsLevelT2;

    // Metodo principal, executado a cada instante de amostragem
    public void fRun() {
        synchronized (this) {
            // Variáveis utilizadas pelo controlador preditivo
            // Devido a planta ser linearizada, trabalha-se com variações
            double[] lastSetPoint;
            double[] lastStep;
            double[] lastLastDeltaXObserver;
            double[] lastDeltaX;
            double[] lastDeltaY;
            double[] lastDeltaU;

            try {
                if (threadActivated) {
                    try {
//                        marcarTempoInicio(TimeStamp.LOOP);

//                        marcarTempoInicio(TimeStamp.LEITURA);
                        lerSaidaPlanta(); // Medindo a saída

                        // OBSERVADOR
                        constrObserver.setDeltaU(u);
                        constrObserver.calcularObservador(y);
                        obsLevelT1 = constrObserver.getXck1();
                        obsLevelT2 = constrObserver.getXck2();

//                        marcarTempoFim(TimeStamp.LEITURA);

                        lastLastDeltaXObserver = lastDeltaXObserver;

                        // Planta real
                        //lastDeltaX = new double[] { levelT1 - deltaXBarra[0], levelT2 - deltaXBarra[1] };
                        lastDeltaX = new double[]{obsLevelT1 - deltaXBarra[0], obsLevelT2 - deltaXBarra[1]};
                        lastDeltaY = new double[]{y - deltaYBarra};
                        lastDeltaU = new double[]{deltaU};

                        lerSetPoint(); // Obtendo o set-point

                        lastSetPoint = new double[]{this.setPoint - deltaYBarra};
                        lastStep = new double[]{step};

//                        lastDeltaXObserver = new double[] { levelT1, levelT2 };
                        lastDeltaXObserver = lastDeltaX;

//                        calcularPerturbacao(lastLastDeltaXObserver, lastDeltaXObserver);

                        if (currentPredictiveController != null) {
//                            marcarTempoInicio(TimeStamp.CONTROLE);
                            // Sinal de controle calculado pelo controlador preditivo
                            deltaU = currentPredictiveController.computeU(lastSetPoint, lastDeltaX, lastDeltaY, lastDeltaU, lastStep)[0];
//                            marcarTempoFim(TimeStamp.CONTROLE);

                            deltaU = arredondar(deltaU, 3);

                            // Calculando o sinal de controle real
                            u = deltaU + deltaUBarra;
                        } else {
                            u = uMA;
                        }

//                        marcarTempoInicio(TimeStamp.ESCRITA);
                        //constrObserver.setSinalEscrito(u);
                        aplicarSinalControle(); // Aplicando o sinal de controle à planta
//                        marcarTempoFim(TimeStamp.ESCRITA);

                        updateGUI();

//                        marcarTempoFim(TimeStamp.LOOP);

//                        printTimeStamps();

                        completarPeriodoAmostragem();
                    } catch (InterruptedException ex) {
                        Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            } catch (PlantCommunicationException ex) {
                Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
            }
        }
    }
    private double deltaUBarra;
    private double[] deltaXBarra;
    private double deltaYBarra;
    private double disturb;
    private double lastDisturb;
    private boolean estabilizou;

    private void calcularPerturbacao(double[] lastLastX, double[] lastX) {
        if (!estabilizou) {
            System.out.println("disturb_atu: " + disturb);
            System.out.println("disturb_ant: " + lastDisturb);

            lastDisturb = disturb;
            disturb = disturbObserver.computeDisturb(lastLastX, lastX, new double[]{u}, lastDisturb);
//            disturb = observer.computeDisturb(lastLastX, lastX, new double[] { deltaU }, lastDisturb);

//            if (Math.abs(disturb - lastDisturb) <= 0.00000000000001) {
            if (Math.abs(disturb - lastDisturb) <= 0.001) {
//            if (disturb == lastDisturb) {
                estabilizou = true;

                // CALCULAR U ATRAVÉS DA RES. DO SISTEMA...
                double[] ok = disturbObserver.solveAXb(disturb, setPoint - 15);
//                deltaYBarra = new double[]{ ok[0], ok[1]};
//                deltaUBarra = ok[2];
                deltaUBarra = plant.getInputOP()[0] + ok[2];
//                deltaUBarra = plant.getInputOP()[0] + observer.solveAXb(disturb, setPoint - plant.getStateOP()[1]);

                System.out.println("deltay1 barra: " + ok[0]);
                System.out.println("deltay2 barra: " + ok[1]);
                System.out.println("deltau: " + deltaU);
                System.out.println("deltauPO: " + plant.getInputOP()[0]);
                System.out.println("deltau barra: " + ok[2]);
                System.out.println("deltau barra final: " + deltaUBarra);
                System.out.println("u final: " + (deltaU + deltaUBarra));
            }
        }
    }

    private void marcarTempoInicio(TimeStamp timeStamp) {
        timeStamps.get(timeStamp)[0] = System.currentTimeMillis();
    }

    private void marcarTempoFim(TimeStamp timeStamp) {
        // Cálculo do tempo restante disponivel no período de amostragem
        timeStamps.get(timeStamp)[1] = System.currentTimeMillis();
    }

    private void completarPeriodoAmostragem() throws InterruptedException {
        long restanteExecucaoLoop = (tS - (timeStamps.get(TimeStamp.LOOP)[1] - timeStamps.get(TimeStamp.LOOP)[0]));

        if (restanteExecucaoLoop > 0) {
            Thread.sleep((int) restanteExecucaoLoop);
        }
    }

    private void printTimeStamps() {
        System.out.println("Levou " + (timeStamps.get(TimeStamp.LEITURA)[1] - timeStamps.get(TimeStamp.LEITURA)[0]) + "ms para LER o sinal de saída.");
        System.out.println("Levou " + (timeStamps.get(TimeStamp.CONTROLE)[1] - timeStamps.get(TimeStamp.CONTROLE)[0]) + "ms para CALCULAR o sinal de controle.");
        System.out.println("Levou " + (timeStamps.get(TimeStamp.ESCRITA)[1] - timeStamps.get(TimeStamp.ESCRITA)[0]) + "ms para ESCREVER o sinal de controle.");
        System.out.println("Ainda RESTARAM " + (tS - (timeStamps.get(TimeStamp.LOOP)[1] - timeStamps.get(TimeStamp.LOOP)[0])) + "ms do período de amostragem.");
        System.out.println("TEMPO " + time + "ms.");
    }

    private void lerSetPoint() {
        double ultimoSetPoint = setPoint;

        setPoint = ui.getSetPoint();

        if (setPoint != ultimoSetPoint) {
            System.out.println("MUDOU SET-POINT: " + setPoint);

            setPointAnterior = ultimoSetPoint;

            step = setPoint - y;

            clearTime();

            zerarEspecificacoesDesempenho();

            clearPlots();

            sentidoReferencia = setPoint < y ? SentidoReferencia.Abaixo : SentidoReferencia.Acima;

            estabilizou = false;
        }
    }

    private void lerSaidaPlanta() throws PlantCommunicationException {
        // Medindo as variáveis de estado (níveis dos tanques)
        double[] levels = plant.readOutput();
//        double[] levels = new double[]{15, 15};

        levelT1 = levels[0];
        levelT2 = levels[1];

        // Medindo a saída (nível do tanque inferior)
        y = levelT2;
    }

    private void aplicarSinalControle() throws PlantCommunicationException {
        limitarNivelT1(); // Trava de segurança do tanque superior
        saturarSinal(); // Saturação do sinal de entrada

//        plant.applyInput(0.6648); // Simulador
//        plant.applyInput(1.25); // Planta real
        plant.applyInput(u); // Aplicando o sinal de controle à planta
        System.out.println("U: " + u); // Aplicando o sinal de controle à planta
    }

    private void updateGUI() {
        // Atualizando o tempo
//        time = System.currentTimeMillis() - inicioExecucao;
        time = (long) ((System.currentTimeMillis() - inicioExecucao) * (500d / tS));

        calcularEspecificacoesDesempenho();

        // Atualizando os campos e gráficos da GUI
        ui.setTempoPico(tempoPico != null ? String.format("%.2f", tempoPico / 1000d) : null);
        ui.setSobressinalMaximo(sobressinalMaximo != null ? String.format("%.2f", sobressinalMaximo) : null);
        ui.setTempoAcomodacao(tempoAcomodacao != null ? String.format("%.2f", tempoAcomodacao / 1000d) : null);
        ui.setNivelT1(String.format("%.2f", levelT1));
        ui.setNivelT2(String.format("%.2f", levelT2));
        ui.setTensao(String.format("%.2f", u));
        signalSetPoint.add(time, ui.getSetPoint());
        signalU.add(time, u);
        signalT1.add(time, levelT1);
        signalT2.add(time, levelT2);

        // OBSERVADOR
        obsSignalT1.add(time, obsLevelT1);
        obsSignalT1.add(time, obsLevelT1);

        if (!tirouPS40 && time / 1000 > 40) {
            takeScreenshot();

            tirouPS40 = true;
        } else if (!tirouPS60 && time / 1000 > 60) {
            takeScreenshot();

            tirouPS60 = true;
        } else if (!tirouPS80 && time / 1000 > 80) {
            takeScreenshot();

            tirouPS80 = true;
        }
    }
    private Long tempoPicoTemp;
    private Double sobressinalMaximoTemp;
    private Long tempoAcomodacaoTemp;

    private void calcularEspecificacoesDesempenho() {
        double valorResposta = arredondar(y, 3);

        long unidadeTempo = time;
        double referencia = arredondar(ui.getSetPoint(), 3);
        double referenciaAnterior = arredondar(setPointAnterior, 3);
        int periodo = tS;
        double derivada = arredondar((valorResposta - valorRespostaAnterior) / periodo, 3);
//        int sinalDerivada = (int) Math.signum(derivada);

        // Tempo de subida
        if (!cruzouReferencia) {
            if ((sentidoReferencia == SentidoReferencia.Acima && valorResposta >= referencia)
                    || (sentidoReferencia == SentidoReferencia.Abaixo && valorResposta <= referencia)) {
                contTesteRuidoTempoSubida++;
            } else {
                contTesteRuidoTempoSubida = 0;
            }
        }

        if (contTesteRuidoTempoSubida == MAX_TESTE_RUIDO_TEMPO_SUBIDA) {
            cruzouReferencia = true;
        }

        // Tempo de pico e máximo sobressinal
        if (tempoPico == null) {
            if (cruzouReferencia
                    && ((sentidoReferencia == SentidoReferencia.Acima && valorResposta >= referencia)
                    || (sentidoReferencia == SentidoReferencia.Abaixo && valorResposta <= referencia))) {
                long tempoPicoAux = unidadeTempo - tempoUltimaMudancaReferencia;
//                double sobressinalMaximoAux = Math.abs(arredondar(((valorResposta - referencia) / step) * 100, 2));
                double sobressinalMaximoAux = Math.abs(arredondar(((valorResposta - referencia) / referencia) * 100, 2));

                if (sobressinalMaximoTemp == null || sobressinalMaximoAux > sobressinalMaximoTemp) {
                    tempoPicoTemp = tempoPicoAux;
                    sobressinalMaximoTemp = sobressinalMaximoAux;

                    contTesteRuidoTempoPico = 0;
                }

                contTesteRuidoTempoPico++;
            }
        }

        if (contTesteRuidoTempoPico == MAX_TESTE_RUIDO_TEMPO_PICO) {
            if (tempoPico == null && sobressinalMaximoTemp > 2) {
                tempoAcomodacao = null;
                tempoAcomodacaoTemp = null;
                contTesteRuidoTempoAcomodacao = 0;
            }

            tempoPico = tempoPicoTemp;

            sobressinalMaximo = sobressinalMaximoTemp;
        }

//        // Tempo de pico e máximo sobressinal
//        if (tempoPico == null && cruzouReferencia && (sinalDerivada == 0 || sinalDerivada != sinalDerivadaAnterior))
////            if (tempoPico == null && cruzouReferencia && (valorResposta < valorRespostaAnterior))
//        {
//            tempoPico = unidadeTempo;
//
//            sobressinalMaximo = Math.abs(arredondar(((valorResposta - referencia) / referencia) * 100, 2));
//        }

        // Tempo de acomodação de 2%
        if (tempoAcomodacao == null) {
//            if (/*derivada == 0 && */(valorResposta >= referencia * 0.98 && valorResposta <= referencia * 1.02)) {
            if (derivada == 0 && valorResposta >= referencia * 0.95 && valorResposta <= referencia * 1.05) {
                if (tempoAcomodacaoTemp == null) {
                    tempoAcomodacaoTemp = unidadeTempo - tempoUltimaMudancaReferencia;
                }

                contTesteRuidoTempoAcomodacao++;
            } else {
                contTesteRuidoTempoAcomodacao = 0;
            }
        }

        if (contTesteRuidoTempoAcomodacao == MAX_TESTE_RUIDO_TEMPO_ACOMODACAO) {
            tempoAcomodacao = tempoAcomodacaoTemp;
        }

        valorRespostaAnterior = valorResposta;
//        sinalDerivadaAnterior = sinalDerivada;
    }

    private void calcularEspecificacoesDesempenhoTempoReal() {
        double valorResposta = arredondar(y, 3);

        long unidadeTempo = time;
        double referencia = arredondar(ui.getSetPoint(), 3);
        double referenciaAnterior = arredondar(setPointAnterior, 3);
        int periodo = tS;
        double derivada = arredondar((valorResposta - valorRespostaAnterior) / periodo, 3);

        // Tempo de subida
        if (!cruzouReferencia) {
            if ((sentidoReferencia == SentidoReferencia.Acima && arredondar(valorResposta, 3) >= referencia)
                    || (sentidoReferencia == SentidoReferencia.Abaixo && arredondar(valorResposta, 3) <= referencia)) {
                cruzouReferencia = true;
            } else {
                cruzouReferencia = false;
            }
        }

        // Tempo de pico e máximo sobressinal
        if (cruzouReferencia
                && ((sentidoReferencia == SentidoReferencia.Acima && arredondar(valorResposta, 3) >= referencia)
                || (sentidoReferencia == SentidoReferencia.Abaixo && arredondar(valorResposta, 3) <= referencia))) {
            long tempoPicoAux = unidadeTempo - tempoUltimaMudancaReferencia;
//            double sobressinalMaximoAux = Math.abs(arredondar(((valorResposta - referencia) / step) * 100, 2));
            double sobressinalMaximoAux = Math.abs(arredondar(((valorResposta - referencia) / referencia) * 100, 2));

            if (sobressinalMaximo == null || sobressinalMaximoAux > sobressinalMaximo) {
                tempoPico = tempoPicoAux;
                sobressinalMaximo = sobressinalMaximoAux;
            }
        }

        // Tempo de acomodação de 2%
//        if (valorResposta >= referencia * 0.98 && valorResposta <= referencia * 1.02) {
        if (derivada == 0 && valorResposta >= referencia * 0.95 && valorResposta <= referencia * 1.05) {
            if (tempoAcomodacao == null) {
                tempoAcomodacao = unidadeTempo - tempoUltimaMudancaReferencia;
            }
        } else {
            tempoAcomodacao = null;
        }

        valorRespostaAnterior = valorResposta;
    }

//    private void calcularEspecificacoesDesempenhoHybrid() {
//        double valorResposta = y;
//        
//        long unidadeTempo = time;
//        double referencia = ui.getSetPoint();
//        int periodo = tS;
////        double derivada = arredondar((valorResposta - valorRespostaAnterior) / periodo, 4);
////        int sinalDerivada = (int) Math.signum(derivada);
//
//        // Tempo de subida
//        if (!cruzouReferencia) {
//            if ((sentidoReferencia == SentidoReferencia.Acima && arredondar(valorResposta, 3) >= referencia) || 
//                    (sentidoReferencia == SentidoReferencia.Abaixo && arredondar(valorResposta, 3) <= referencia)) {
//                cruzouReferencia = true;
//            } else {
//                cruzouReferencia = false;
//            }
//        }
//
//        // Tempo de pico e máximo sobressinal
//        if (cruzouReferencia && 
//                ((sentidoReferencia == SentidoReferencia.Acima && arredondar(valorResposta, 3) >= referencia) || 
//                (sentidoReferencia == SentidoReferencia.Abaixo && arredondar(valorResposta, 3) <= referencia))) {
//            long tempoPicoAux = unidadeTempo - tempoUltimaMudancaReferencia;
//            double sobressinalMaximoAux = Math.abs(arredondar(((valorResposta - referencia) / referencia) * 100, 2));
//
//            if (sobressinalMaximo == null || sobressinalMaximoAux > sobressinalMaximo) {
//                tempoPico = tempoPicoAux;
//                sobressinalMaximo = sobressinalMaximoAux;
//
//                if (sobressinalMaximo > 2) {
//                    tempoAcomodacao = null;
//                    tempoAcomodacaoTemp = null;
//                    contTesteRuidoTempoAcomodacao = 0;
//                }
//            }
//        }
//        
////        // Tempo de pico e máximo sobressinal
////        if (tempoPico == null && cruzouReferencia && (sinalDerivada == 0 || sinalDerivada != sinalDerivadaAnterior))
//////            if (tempoPico == null && cruzouReferencia && (valorResposta < valorRespostaAnterior))
////        {
////            tempoPico = unidadeTempo;
////
////            sobressinalMaximo = Math.abs(arredondar(((valorResposta - referencia) / referencia) * 100, 2));
////        }
//
//        // Tempo de acomodação de 2%
//        if (tempoAcomodacao == null) {
//            if (/*derivada == 0 && */(valorResposta >= referencia * 0.98 && valorResposta <= referencia * 1.02)) {
//                if (tempoAcomodacaoTemp == null) {
//                    tempoAcomodacaoTemp = unidadeTempo - tempoUltimaMudancaReferencia;
//                }
//                
//                contTesteRuidoTempoAcomodacao++;
//            } else {
//                contTesteRuidoTempoAcomodacao = 0;
//            }
//        }
//        
//        if (contTesteRuidoTempoAcomodacao == MAX_TESTE_RUIDO_TEMPO_ACOMODACAO) {
//            tempoAcomodacao = tempoAcomodacaoTemp;
//        }
//
//        valorRespostaAnterior = valorResposta;
////        sinalDerivadaAnterior = sinalDerivada;
//    }
    private enum SentidoReferencia {

        Abaixo,
        Acima
    }
    private long tempoUltimaMudancaReferencia;
    private SentidoReferencia sentidoReferencia;

    private void zerarEspecificacoesDesempenho() {
        tempoPico = null;
        sobressinalMaximo = null;
        tempoAcomodacao = null;
        tempoPicoTemp = null;
        sobressinalMaximoTemp = null;
        tempoAcomodacaoTemp = null;
        tempoUltimaMudancaReferencia = time;

        contTesteRuidoTempoSubida = 0;
        contTesteRuidoTempoPico = 0;
        contTesteRuidoTempoAcomodacao = 0;

        cruzouReferencia = false;

        tirouPS40 = false;
        tirouPS60 = false;
        tirouPS80 = false;
    }

    private static double arredondar(double numero, int casasDecimais) {
        BigDecimal bd = new BigDecimal(numero);

        return bd.setScale(casasDecimais, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
//    // Metodo principal, executado a cada instante de amostragem
//    public void fRun() {
//        // Variáveis referentes ao observador de estados
////        double d;
////        double disturb, disturb_ant;
////        double[] lastLastX = new double[]{0, 0, 0}; // NOVO;
////        double deltaUOffSet = 0;
////        boolean estabilizou = false;
//        // Variáveis referentes ao controlador
////        // Utilizando o modelo
////        //double[] lastXModel = new double[2];
////        double[] lastXModel = new double[3]; // NOVO
////        double[] nextXModel;
////        // Uma saida: nível do tanque inferior
////        double[] lastYModel = new double[1];
//        // Utilizando a planta real
//        //private double[] lastX = new double[]{0, 0};
//        // Saida: nivel do tanque 1 OU 2
//        double y;
//        double[] lastX; // NOVO
//        double[] lastY;
//        double[] lastU;
//        double deltaU = 0;
////        double u_ant = 0;
//        
//        try {
//            if (threadActivated) {
//                try {
//                    long inicio = System.currentTimeMillis();
//                    double[] setPoint = ui.getSetPoint();
//
////                    lastLastX = lastX;
//
//                    // Medindo as variáveis de estado
//                    //nivelT1 = lastXModel[0] * CTE_TANQUE;
//                    //nivelT2 = lastXModel[1] * CTE_TANQUE;
//                    levelT1 = quanserClient.read(portT1) * Constants.CTE_TANQUE;
//                    levelT2 = quanserClient.read(portT2) * Constants.CTE_TANQUE;
//
//                    // Medindo a variável de saída (nível do tanque inferior)
//                    y = levelT2;
//
//                    // Planta real
//                    //lastX = new double[]{levelT1 - PO, levelT2 - PO};
//                    lastX = new double[] { levelT1 - plant.getStateOP()[0], levelT2 - plant.getStateOP()[1] };
//                    lastY = new double[] { y - plant.getStateOP()[1] };
//                    lastU = new double[] { deltaU };
//
////                    if (!estabilizou) {
////                        System.out.println("disturb: " + disturb);
////                        System.out.println("disturb_ant: " + disturb_ant);
////                        disturb_ant = disturb;
////                        disturb = observer.computeDisturb(lastLastX, lastX, new double[]{u_ant}, disturb_ant);
////                        if (Math.abs(disturb - disturb_ant) <= 0.00001) {
////                            estabilizou = true;
////                            // CALCULAR U ATRAVÉS DA RES. DO SISTEMA...
////                            deltaUOffSet = observer.solveAXb(disturb);
////                            System.out.println("delta offset: " + deltaUOffSet);
////                        } else {
////                            deltaUOffSet = 0;
////                        }
////                    }
//
//                    // Modelo
//                    //nextXModel = plant.computeXModel(lastXModel, new double[]{uReal});
//                    //lastYModel = plant.computeYModel(lastXModel);
//                    //lastXModel = nextXModel;
//
//                    double tempoInicio = System.currentTimeMillis();
//
//                    //setPoint[0] = setPoint[0] - PO; PORQUE - PO???
//
//                    // TENTATIVA DE REDUÇÃO DO ERRO (NÃO FUNCIONOU)
//                    // Diferença entre a saída da planta e a saída do modelo (d)
//                    //d = lastY[0] - PO - lastYModel[0];
//                    //u = currentPredictiveController.computeU(setPoint, lastX, lastY, lastU, d)[0];
//
//                    // SINAL DE CONTROLE CALCULADO PELO CONTROLADOR
//                    deltaU = currentPredictiveController.computeU(setPoint, lastX, lastY, lastU)[0];
//
//                    double tempoFim = System.currentTimeMillis();
//                    //System.out.println("Levou " + ((tempoFim - tempoInicio) / 1000d) + " segundos para calcular o sinal de controle.");
//
////                    u_ant = deltaU; // u(k) = u(k+1)
//
//                    //uReal = (deltaU + U0) / CTE_MODULO_POTENCIA;
////                    uReal = (deltaU + plant.getInputOP()[0] + deltaUOffSet) / Constants.CTE_MODULO_POTENCIA;
//                    u = (deltaU + plant.getInputOP()[0]) / Constants.CTE_MODULO_POTENCIA;
//
//                    limitarNivelT1(); // Trava de segurança do tanque superior
//                    saturarSinal(); // Saturação do sinal de entrada
//
//                    quanserClient.write(portWrite, u); // APLICANDO O SINAL DE CONTROLE À PLANTA
//
//                    updateGUI(); // Atualizando os campos e gráficos da GUI
//
//                    time += tS; // ATUALIZANDO O TEMPO
//
//                    // Calculo do tempo restante disponivel no periodo de amostragem
//                    double fim = System.currentTimeMillis();
//                    double restante = tS - (fim - inicio);
//
//                    //System.out.println("Ainda restaram " + restante + " ms do período de amostragem.");
//
//                    if (restante > 0) {
//                        Thread.sleep((int) restante);
//                    }
//
//                } catch (InterruptedException ex) {
//                    Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
//                }
//            }
//
//        } catch (QuanserClientException ex) {
//            Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
//        } finally {
//        }
//
//    }
//    double[] lastDeltaXModel = new double[] { 0, 0 };
//    double d, e, yModel, deltaUAnterior, u2;
//
//    // Metodo principal, executado a cada instante de amostragem
//    public void fRun() {
//        // Variáveis utilizadas pelo controlador preditivo
//        // Devido a planta ser linearizada, trabalha-se com variações
//        double[] lastDeltaX;
//        double[] lastDeltaY;
//        double[] lastDeltaU;
//        
//        try {
//            if (threadActivated) {
//                try {
//                    marcarTempoInicioLoop();
//                    
//                    double[] setPoint = new double[]{ ui.getSetPoint() - plant.getStateOP()[1] };
//                    
//                    lerSaidaPlanta(); // Medindo a saída
//                    
//                    yModel = plant.computeYModel(lastDeltaXModel)[0];
//
//                    // Planta real
//                    lastDeltaX = new double[] { levelT1 - plant.getStateOP()[0], levelT2 - plant.getStateOP()[1] };
//                    lastDeltaY = new double[] { y - plant.getStateOP()[1] };
//                    lastDeltaU = new double[] { deltaU };
//
//                    marcarTempoInicioControlador();
//                    
//                    d = y - yModel;
//                    e = setPoint[0] - yModel;
//                    deltaUAnterior = deltaU;
//                    
//                    setPoint[0] = setPoint[0] - d;
//                    
//                    System.out.println("VALOR DA DISTURBAÇÃO: " + d);
//                    System.out.println("VALOR DO ERRO: " + e);
//
//                    // Sinal de controle calculado pelo controlador preditivo
//                    deltaU = currentPredictiveController.computeU(setPoint, lastDeltaX, lastDeltaY, lastDeltaU)[0];
//                    
//                    System.out.println("VALOR DO DELTAU: " + (deltaU - deltaUAnterior));
//                    
//                    marcarTempoFimControlador();
//
//                    // Calculando o sinal de controle real
//                    u = (deltaU + plant.getInputOP()[0]) / Constants.CTE_MODULO_POTENCIA;
//                    u2 = (deltaU + plant.getInputOP()[0]);
//
//                    aplicarSinalControle(); // Aplicando o sinal de controle à planta
//                    
//                    // Simulação do modelo
//                    if (u2 > 3) {
//                        u2 = 3;
//                    } else if (u2 < -3) {
//                        u2 = -3;
//                    }
//                    lastDeltaXModel = plant.computeXModel(lastDeltaXModel, new double[] { u2 });
//
//                    updateGUI();
//
//                    marcarTempoFimLoop();
//
//                    if (restanteExecucaoLoop > 0) {
//                        Thread.sleep((int) restanteExecucaoLoop);
//                    }
//
//                } catch (InterruptedException ex) {
//                    Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
//                }
//            }
//
//        } catch (QuanserClientException ex) {
//            Logger.getLogger(FThread.class.getName()).log(Level.SEVERE, null, ex);
//        } finally {
//        }
//
//    }
}
